'''
Contains context-handling algorithms.

@author: Oraby
'''
try:
    import multiprocessing
except ImportError:
    multiprocessing = None
from twistedSim import simManager
from twistedSim.override._socket import MasterNode

#In contrast to best practice recommendations, some imported modules are
# not defines globally. Instead, these modules are imported inside functions
# and methods. This is done because if these modules were imported globally,
# then they would corrupt the context-handling algorithms used. 


class ProcessHandler(simManager.Module, MasterNode):
    isProcessBased = True
    _pipesDict = {}
    
    def _collect():
        RuntimeWarning("This is no use and it's just dummy," +\
                       " to be fixed later")
        while ProcessHandler._pipesDict:
            for _id, (_object, pipe) in ProcessHandler._pipesDict.items():
                while pipe.poll():
                    methodName, args, kargs = pipe.recv()
                    method = getattr(_object, methodName)
                    method(*args, **kargs)
                if _object._done:
                    del ProcessHandler._pipesDict[_id]
    _collect = staticmethod(_collect)
        
    def load(self, machine):
        if not multiprocessing:
            errMsg =  "Failed to import Python's multiprocessing module.\n"
            errMsg += "Make sure that the used Python version and "
            errMsg += "distribution supports the multiprocessing module."
            raise RuntimeError(errMsg)
        
        self.setOwner(machine)
        
        #Prepare attribute needed for emulated module proxy.
        attrs = self.getAttrs()
        emuModuleParams = [attrs]
        
        #Prepare attributes needed for emulated machine proxy
        self._machineProxy = self.initializeMasterNode(machine, self)
        #TODO: send skts reps instead.
        emuMachineParam = [machine.getInterfacesIP(), machine._skts, 
            self._machineProxy.getPortRange(), machine.getMachineName()]
        
        #Prepare some parameters that are globally used.
        from twistedSim.override._time import SimTimeSingleton
        timeNow = SimTimeSingleton.getTime()
        self._pipe, otherEnd = multiprocessing.Pipe()
        globalParams = [timeNow, otherEnd]
        p = multiprocessing.Process(target = _remoteLoad, 
                                    name = self._nickName,
                                    args = (globalParams, emuMachineParam,
                                            emuModuleParams))
        p.daemon = True
        p.start()   
        myID = self.getId()
        ProcessHandler._pipesDict[myID] = self, self._pipe
        self._done = False
        def terminateProcess(p):
            p.terminate()
        simManager.SimManager.addPostRun(terminateProcess, p)
        ProcessHandler._collect()


    def runAsEvent(self):
        if self.dead:#Don't waste time with dead applications.
            return
        self.ImmediateScheduleDown()
        self._event = None
        

        from twistedSim.override._time import SimTimeSingleton
        currentTime = SimTimeSingleton.getTime()
        self._machineProxy._updateSktMappingOut()
        self.executeRemote("trigerIteration", [currentTime], {})
        
        ProcessHandler._pipesDict[self._id] = self, self._pipe
        self._done = False
        ProcessHandler._collect()

    def _handleTimeout(self, timeout):
        if timeout == -1:
            #Declare the guy dead gracefully, i.e. The guy
            # closed the sockets he's connected to by himself.
            self.die()
        else:
            self._timeout = timeout
            self._fixSchd() 

    def die(self):
        if self.dead:
            print "Module " + self._moduleName + " is already dead."
        else:
            if self.isRunning():
                machine = self.getOwner()
                machine.removeModule(self)
            if self.time != None:
                try:
                    from twistedSim.Scheduler import SchedulerSingleton
                    SchedulerSingleton.removeEvent(self.time, self._event)
                except (KeyError,ValueError), e:
                    #I may be in the same cycle so it wasn't
                    #assigned to None yet.
                    pass
                self.time = None
            self.dead = True
            #TODO: Create debug options for verbosity
            #print "Module " + self._nickName + " died."
            del self._fileno #Delete fds references
            self._owner = None    

    def _remoteIterationDone(self):
        self._done = True


    def executeRemote(self, methodName, args = [], kargs = {}):
        self._pipe.send((methodName,args, kargs))

def _remoteLoad(glolbalParams, machineParams, moduleParams):
    '''
    This function is the entry point of the emulated node. I.e, this
    function is the first thing called on the emulated node.
    '''
    _remoteProxy(glolbalParams, machineParams, moduleParams)


class _remoteProxy():
    '''
    This class is created and instantiated on the emulated node's process 
    on its launch, the class is responsible for carrying out the
    communication tasks with the ProcessHandler instance on the master
    node.
    
    The class can be pictured as the master node's event-loop's
    representative on the emulated node.
    '''
    _count = 0 #Used in debugging
    
    def __init__(self, glolbalParams, machineParams,  moduleParams):
        #Set global params
        initialTime, pipeEnd = glolbalParams
        from twistedSim.override._time import SimTimeSingleton
        SimTimeSingleton.setTime(initialTime)
        _remoteProxy._pipe = pipeEnd
        from twistedSim.override import _socket
        _socket._proxy = _socket._specialClass()
        
        #Set machine params
        _socket._proxy.machine = _socket.EmulatedNode(
              *machineParams + [_remoteProxy.executeRemote])
                
        self._thisNode = _remoteModule.EmulatedNode(*moduleParams + 
                                            [_socket._proxy.machine])
        _remoteProxy._nickName = self._thisNode.getNickname()
        self._run()
        
    def _run(self):
        '''
        This method is the event-loop that keeps running on the emulated
        node. 
        
        The method simply receives commands from master node's pipe and
        triggers them. 
        '''
        self._thisNode.load()
        while self._pipe.poll(None):
            #Can it be anything other than a single iteration run??
            methodName, args, kargs = self._pipe.recv()
            method = getattr(self, methodName)
            method(*args, **kargs)
    
    def trigerIteration(self, currentTime):
        '''
        When called, the method triggers a single iteration on 
        on the emulated node. 
        '''
        from twistedSim.override._time import SimTimeSingleton
        SimTimeSingleton.setTime(currentTime)
        self._thisNode.runAsEvent()
        
        #TODO: Update the master machine with the socket that I might
        # had binded to.
        
    def _updateSktMappingIn(self, *args, **kargs):
        from twistedSim.override._socket import _getCurrentMachine
        machine = _getCurrentMachine()
        machine._updateSktMappingIn(*args, **kargs)
    
    def pushNetworkMsg(self, *args, **kargs):
        '''
        When an emulated socket sends a network message, it calls
        this function to deliver the message for him.
        '''
        from twistedSim.override._socket import _getCurrentMachine
        machine = _getCurrentMachine()
        machine.pushNetworkMsg(*args, **kargs)
    
    
    def executeRemote(methodName, args = [], kargs = {}):
        '''
        Executes a given method with a given parameters on the master node.
        The given method must exist in the ProcessHandler instance called
        on the master node.
        '''
        _remoteProxy._count += 1
        import sys
        sys.stdout.flush()
        sys.stderr.flush() 
        _remoteProxy._pipe.send((methodName, args, kargs))
    executeRemote = staticmethod(executeRemote)




class _remoteModule:
    '''
    The classes here contains all classes and functions runs on the remote
    emulated node. 
    
    Currently not all remote emulated node code exists yet  this class, some
    are scattered outside this class (however expect it to move here soon).
    '''


    class EmulatedNode():
        '''
        The class represents the TwistedSim application handling part on the
        emulated node process.  
        '''
        isProcessBased = True
        
        def __init__(self, attrs, machine):
            self._attrs = attrs
            self._machine = machine
            self._fileno = {}
            self._filenoID = 1
            self._dead = False
        
        def _iterationDone(self):
            '''
            The method is called to signal that we finished our single
            iteration.
            
            The method just notifies the master node that this emulated
            application finished its iteration.
            '''
            if  not self._dead:
                self._machine._updateSktMappingOut()
            _remoteProxy.executeRemote("_remoteIterationDone", [], {})
    
        
        def load(self):
            '''
            When called, the method is responsible for actually loading
            the emulated application for the first time.
            '''
            from twistedSim.override import _socket
            _socket._proxy.module = self
            
            self._handler = self._attrs['moduleHandler']
            self._handler.setModule(self)
            
            self._handler.preSwitchContext(True)
            self._handler.postSwitchContext(True)
            self._handler.preLoad()
            
            try:
                import sys
                sys.argv = self._attrs['sysArgs']
                appDir = self._attrs['modulePath']
                sys.path += appDir
                simManager.installStdStreams(self._attrs['nickName'])
                import os
                os.chdir(appDir)
                filePath = appDir + "/" + self._attrs['moduleName']# + ".py"
                from imp import load_source
                loader = load_source("__main__",filePath)
            finally:
                self._handler.postLoad()
            self._iterationDone()
        
        def runAsEvent(self):
            '''
            Trigger a single iteration of the application event-loop.
            '''
            self._handler.preSwitchContext()
            self._handler.postSwitchContext()
            self._handler.runUntilCurrent() #Make the actual run
            timeout = self._handler.timeout()
            if timeout == -1:
                self.die()
            _remoteProxy.executeRemote("_handleTimeout", [timeout])
            self._handler.preSwitchOriginal()
            self._handler.postSwitchOriginal()
            self._iterationDone()

        def die(self):
            '''
            Part of this method is handled on this emulated node.
            and the other half is handled on the module representative
            on the master node.
            '''
            import socket
            for fileDesc in self._fileno:
                if type(fileDesc) == socket.socket:
                    fileDesc.close()
            self._machine._updateSktMappingOut()
            self._dead = True
        
        def ImmediateScheduleRaise(self):
            '''
            Schedule this application to be run again in the soonest
            time possible.
            '''
            _remoteProxy.executeRemote("ImmediateScheduleRaise", [], {})
            
        def getNickname(self):
            '''
            Returns the Nickname associated with the Module.
            '''
            return self._attrs["nickName"]
    

        def createFileDesc(self, fileObject):
            '''
            Associate a file object with the module and return a file descriptor
            for it. This method shouldn't be called by the user, it is rather 
            called by the emulated sockets method.
            
            The method doesn't check for error.
           
            @type fileObject: C{object}
            @param fileObject: The object itself.
            
            @rtype fileno: C{int}
            @return fileno: The file descriptor.
            '''
            fileNo = self._filenoID
            self._filenoID += 1
            self._fileno[fileNo] = fileObject
            return fileNo
           
        def getFileDesc(self, fileno):
            '''
            Returns the object associated with a file descriptor.
           
            @type fileDesc: C{fileno}
            @param fileDesc: The file descriptor to be retrieved.
           
            @rtype: C{object}
            @return: The file object.
            '''
            return self._fileno[fileno]
    
        def isFileDescExists(self, fileno):
            '''
            Returns whether or not a file descriptor exists or not.
            
            @rtype: C{True|False}
            @return: True or False.
            '''
            return fileno in self._fileno
    
        def removeFileDesc(self, fileno):
            '''
            Remove a file descriptor association with the module.
           
            @type fileno: C{int}
            @param fileno: The file descriptor to be removed.
            '''
            del self._fileno[fileno]
            
        def getId(self):
            '''
            Returns the ID of the module on the L{SimManager.Machine} it
            is running on.
            '''
            return self._attrs['id']